import type { Metadata } from 'next'; import Link from 'next/link'; import { AdminTitle, JsonPre, KindChip, Mono, Notice, StatusChip } from '@/components/admin/ui'; import { SectionNav } from '@/components/layout/terminal'; import { EntityBadge, TierBadge } from '@/components/ui/badges'; import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table'; import { Note } from '@/components/ui/section'; import { Unavailable } from '@/components/ui/unavailable'; import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api'; import type { ExtractionPayload, ExtractionSpan } from '@/lib/admin/types'; import { cn } from '@/lib/cn'; import { fmtBytes, fmtDateTime, fmtInt, fmtValue, num } from '@/lib/format'; import { routes } from '@/lib/site'; export const metadata: Metadata = { title: 'Extraction debugger', robots: { index: false, follow: false } }; export const dynamic = 'force-dynamic'; const STAGES = [ { id: 'raw', label: 'Raw' }, { id: 'normalized', label: 'Normalized' }, { id: 'deterministic', label: 'Deterministic' }, { id: 'llm', label: 'LLM' }, { id: 'candidates', label: 'Candidates' }, { id: 'claims', label: 'Claims' }, { id: 'relations', label: 'Relations' }, { id: 'reconciliation', label: 'Reconciliation' }, { id: 'events', label: 'Events' }, ]; /** Text excerpt with the located claim values highlighted (offsets are into the cleaned text). */ function HighlightedText({ text, spans }: { text: string; spans: ExtractionSpan[] }) { const found = spans.filter((s) => s.found && typeof s.offset === 'number' && s.offset >= 0 && s.offset < text.length && s.match).sort((a, b) => (a.offset ?? 0) - (b.offset ?? 0)); const parts: React.ReactNode[] = []; let cursor = 0; for (const s of found) { const start = s.offset as number; const len = (s.match as string).length; if (start < cursor) continue; parts.push(text.slice(cursor, start)); parts.push( {text.slice(start, start + len)} , ); cursor = start + len; } parts.push(text.slice(cursor)); return <>{parts}; } function Stage({ id, title, count, children, lede }: { id: string; title: string; count?: number | null; children: React.ReactNode; lede?: string }) { return (

{title} {count !== undefined && {fmtInt(count)}}

{lede &&

{lede}

}
{children}
); } function ClaimsTable({ claims, spans, caption }: { claims: ExtractionPayload['claims']; spans: ExtractionSpan[]; caption: string }) { const spanById = new Map(spans.map((s) => [s.claim_id, s])); return ( Entity Property Value Raw Status Conf. Tier Located in text Claim {claims.length === 0 && No claims at this stage.} {claims.map((c) => { const s = spanById.get(c.id); return ( {c.entity_slug ? {c.entity_slug} : {c.entity_id ?? '—'}} {c.property} {fmtValue(c.value, c.property)}{c.unit ? {c.unit} : null} {c.value_raw === null || c.value_raw === undefined ? '—' : String(c.value_raw)} {c.confidence} {s?.found ? ( found @ {s.offset} ) : s ? ( not found ) : ( — )} {c.id} ); })} ); } export default async function ExtractionPage({ params, searchParams }: { params: Promise<{ snapshot: string }>; searchParams: Promise> }) { await requireAdmin(); const { snapshot } = await params; const sp = await searchParams; const res = await load(adminApi.extraction(snapshot, 20000)); if (!res.ok) { return ( <> ); } const x = res.data; const det = x.claims.filter((c) => c.extractor !== 'llm'); const llm = x.claims.filter((c) => c.extractor === 'llm'); const text = x.text ?? ''; return ( <> {x.id}} lede="The pipeline for one snapshot: raw fetch → cleaned text → deterministic claims → LLM claims → entity candidates → claims written → relations → reconciliation with the previous snapshot → events. Value locations are a best-effort search in the text; not-found is reported, never inferred."> Snapshot record → {x.document_id && ( Document → )}
Observed
{fmtDateTime(x.observed_at)}
HTTP · type · size
{fmtValue(x.http_status)} · {x.content_type ?? '—'} · {fmtBytes(x.byte_size)}
Hashes
content {x.content_hash?.slice(0, 16) ?? '—'} text {x.text_hash?.slice(0, 16) ?? '—'}
Connector · parser · transport
{x.connector_name ?? '—'} parser v{x.parser_version ?? '—'} {x.transport ?? '—'}
Run · document
{x.run_id ?? '—'} · {x.document_id} {x.doc_type && }
Entity
{x.entity_slug ? ( {x.entity_name ?? x.entity_slug} ) : ( '—' )}
Flags
raw {x.has_raw ? 'archived' : '—'} · text {x.has_text ? 'yes' : '—'} · structured {x.has_structured || x.structured ? 'yes' : '—'} · changed {x.changed ? 'yes' : 'no'} ·
{x.structured != null && (
Structured data (JSON-LD, OG, embedded JSON)
)}
{text ? (
            
          
) : (

No cleaned text{x.text_error ? ` — ${x.text_error}` : ''}.

)}
{x.llm_jobs.length > 0 && } {x.llm_jobs.length === 0 && llm.length === 0 ?

No LLM extraction for this snapshot (deterministic only).

: }
Entity Type Identity Merged into Id {x.entity_candidates.length === 0 && No candidate.} {x.entity_candidates.map((c) => ( {c.canonical_name} {c.identity_confidence ?? '—'} {c.merged_into ? {c.merged_into} : —} {c.id} ))} {(x.results.length > 0 || x.prices.length > 0) && (

Benchmark results {fmtInt(x.results.length)}

Prices {fmtInt(x.prices.length)}

)}
{x.relations.length ? :

No relation written from this snapshot.

}
{x.previous_snapshot ? (
Previous snapshot
{x.previous_snapshot.id} {' '} {fmtDateTime(x.previous_snapshot.observed_at)}
Content changed
{x.previous_snapshot.content_hash && x.content_hash ? (x.previous_snapshot.content_hash === x.content_hash ? 'no (same hash)' : 'yes (hash differs)') : '—'}
Claim outcomes
{['current', 'superseded', 'conflicting', 'retracted'].map((s) => ( c.status === s) && 'text-danger')}> {s} {fmtInt(x.claims.filter((c) => c.status === s).length)} ))}
) : (

First snapshot of this document — nothing to reconcile against.

)} {x.diff != null && (
Diff payload
)}
{x.events.length ? :

No event emitted.

}
{x.note && {x.note}} ); }